#!/usr/bin/env python
# -*- coding: utf-8 -*-

# @Time : 2019-12-09 下午 1:13
# @Author : 赵兵坡
# @File : mcp23017.py
# @Software: PyCharm
# @Des: MCP23017驱动模块

import sys
import struct
import time
import threading

if sys.platform.startswith("linux"):
    import smbus
else:
    smbus = None


class MCP23017:
    """Initialize MCP23017 instance on specified I2C bus and optionally
    at the specified I2C address.
    GPIO分布定义
    | BIT15 | BIT14 | BIT13 | BIT12 | BIT11 | BIT10 | BIT09 | BIT08 | BIT07 | BIT06 | BIT05 | BIT04 | BIT03 | BIT02 | BIT01 | BIT00 |
    |  GB7  |  GB6  |  GB5  |  GB4  |  GB3  |  GB2  |  GB1  |  GB0  |  GA7  |  GA6  |  GA5  |  GA4  |  GA3  |  GA2  |  GA1  |  GA0  |
    """
    def __init__(self):
        pass

    # 寄存器地址定义
    ADDRESS = 0x20
    REG_IODIRA = 0x00
    REG_IODIRB = 0x01
    REG_IPOLA = 0x02
    REG_IPOLB = 0x03
    REG_GPINTENA = 0x04
    REG_GPINTENB = 0x05
    REG_DEFVALA = 0x06
    REG_DEFVALB = 0x07
    REG_INTCONA = 0x08
    REG_INTCONB = 0x09
    REG_IOCON = 0x0A
    REG_GPPUA = 0x0C
    REG_GPPUB = 0x0D
    REG_INTFA = 0x0E
    REG_INTFB = 0x0F
    REG_INTCAPA = 0x10
    REG_INTCAPB = 0x11
    REG_GPIOA = 0x12
    REG_GPIOB = 0x13
    REG_OLATA = 0x14
    REG_OLATB = 0x15

    PA0, PA1, PA2, PA3, PA4, PA5, PA6, PA7, PB0, PB1, PB2, PB3, PB4, PB5, PB6, PB7, PA_ALL, PB_ALL, ALL = \
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, \
        [0, 1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14, 15], \
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

    IO0, IO1, IO2, IO3, IO4, IO5, IO6, IO7, IO8, IO9, IO10, IO11, IO12, IO13, IO14, IO15 = \
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15

    LOW, HIGH = 0, 1
    OUT, IN = 0, 1
    NOINT, RISING, FALLING, BOTH = 0, 1, 2, 3

    intf_en = False
    lock = threading.Lock()
    last_in = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    interrupt_pins = {}

    @staticmethod
    def __read_reg16(addr, reg):
        """
        读取16位寄存器
        :param addr: mcp芯片地址
        :param reg: 操作寄存器地址
        :return: 返回寄存器值
        """
        if smbus is None:
            return None
        try:
            iic = smbus.SMBus(1)
            buf = iic.read_i2c_block_data(addr, reg, 2)
            return buf[1] << 8 | buf[0]
        except Exception as err:
            print('I2C Error:%s' % err.args.__str__())
            return None

    @staticmethod
    def __wirte_reg16_bit(addr, reg, pins, val):
        """
        按位写16位寄存器
        :param addr: mcp芯片地址
        :param reg: 操作寄存器地址
        :param pins: 引脚编号，可以是单位引脚，也可以位多个引脚组成的列表
        :param val: 引脚的设定值
        :return: 无
        """
        if smbus is None:
            return
        try:
            dirv = MCP23017.__read_reg16(addr, reg)
            if dirv is None:
                return

            if isinstance(pins, int):
                if val == 0:
                    dirv = dirv & (~(1 << pins))
                else:
                    dirv = dirv | (1 << pins)
            elif isinstance(pins, list):
                for i in range(len(pins)):
                    if val == 0:
                        dirv = dirv & (~(1 << pins[i]))
                    else:
                        dirv = dirv | (1 << pins[i])
            else:
                raise Exception('pins必须为int或list。')
            buf = struct.pack('<H', dirv)
            iic = smbus.SMBus(1)
            iic.write_i2c_block_data(addr, reg, [buf[0], buf[1]])
        except Exception as err:
            print('I2C Error:%s' % err.args.__str__())

    # 获取GPIO方向设置数据
    @staticmethod
    def get_dir(addr, pin=None):
        """
        获取MCP的引脚方向设定值
        :param addr: mcp芯片地址
        :param pin: 指定引脚
        :return: 方向设定值
        """
        ps = MCP23017.__read_reg16(addr, MCP23017.REG_IODIRA)
        if pin is None:
            return ps
        else:
            if ps & (1 << pin) != 0:
                return MCP23017.IN
            else:
                return MCP23017.OUT

    # 设置GPIO的输出方向
    @staticmethod
    def set_dir(addr, pins, val):
        """
        设置引脚的输入输出方向
        :param addr: mcp芯片地址
        :param pins: 引脚编号，可以是单位引脚，也可以位多个引脚组成的列表
        :param val: 引脚的IO方向
        :return: 无
        """
        return MCP23017.__wirte_reg16_bit(addr, MCP23017.REG_IODIRA, pins, val)

    # 设置GPIO的输出方向
    @staticmethod
    def set_ipu(addr, pins, val):
        """
        设置引脚的输入输出方向
        :param addr: mcp芯片地址
        :param pins: 引脚编号，可以是单位引脚，也可以位多个引脚组成的列表
        :param val: 引脚的IO方向
        :return: 无
        """
        return MCP23017.__wirte_reg16_bit(addr, MCP23017.REG_GPPUA, pins, val)

    @staticmethod
    def get_out(addr, pin=None):
        """
        获取MCP的引脚输出设定值
        :param addr: mcp芯片地址
        :param pin: 指定引脚
        :return: 输出设定值，如指定了pin则返回引脚状态
        """
        ps = MCP23017.__read_reg16(addr, MCP23017.REG_GPIOA)
        if pin is None:
            return 0
        else:
            if ps & (1 << pin) != 0:
                return MCP23017.HIGH
            else:
                return MCP23017.LOW

    @staticmethod
    def set_out(addr, pins, val):
        """
        设置引脚的输出状态
        :param addr: mcp芯片地址
        :param pins: 引脚编号，可以是单位引脚，也可以位多个引脚组成的列表
        :param val: 引脚输出电平
        :return: 无
        """
        return MCP23017.__wirte_reg16_bit(addr, MCP23017.REG_GPIOA, pins, val)

    @staticmethod
    def __check_interrupt(bouncetime):
        """
        中断检测函数
        :param bouncetime: 去抖时间，单位ms, 最小为50ms
        :return: 无
        """
        if smbus is None:
            return
        if not MCP23017.intf_en:
            return

        if bouncetime < 50:
            bouncetime = 50

        threading.Timer(bouncetime / 1000, MCP23017.__check_interrupt,
                        args=(bouncetime, )).start()

        MCP23017.lock.acquire()
        # print('当前输入信号为%04X' % cur)
        for s_addr in MCP23017.interrupt_pins:
            addr = int(s_addr, 16)
            cur = MCP23017.get_out(addr)
            for s_pin in MCP23017.interrupt_pins[s_addr]:
                pin = int(s_pin)
                callback = MCP23017.interrupt_pins[s_addr][s_pin]['callback']
                edge = MCP23017.interrupt_pins[s_addr][s_pin]['edge']
                last = MCP23017.interrupt_pins[s_addr][s_pin]['last']
                ps = (cur >> pin) & 0x01
                if edge == MCP23017.RISING:  # 上升沿
                    if ps > last and ps != last:
                        threading.Thread(target=callback, args=(addr, pin, ps, last)).start()
                elif edge == MCP23017.FALLING:  # 下降沿
                    if ps < last and ps != last:
                        threading.Thread(target=callback, args=(addr, pin, ps, last)).start()
                else:
                    if ps != last:
                        threading.Thread(target=callback, args=(addr, pin, ps, last)).start()
                MCP23017.interrupt_pins[s_addr][s_pin]['last'] = ps
        MCP23017.lock.release()

    @staticmethod
    def set_interrupt(addr, pins, edge, callback=None, bouncetime=100):
        """
        设置引脚的中断方式
        :param addr: mcp芯片地址
        :param pins: 引脚编号，可以是单引脚，也可以位多个引脚组成的列表
        :param edge: 中断方式，可以选择NOINT, RISING, FALLING, BOTH
        :param callback: 回调函数
        :param bouncetime: 去抖时间，单位ms, 最小为50ms，这个时间将会更改所有已经存在的中断检测。
        :return: 无
        """
        if smbus is None:
            return
        if isinstance(pins, int):
            pins = [pins]

        s_addr = "%02X" % addr
        for pin in pins:
            s_pin = '%02d' % pin
            if not (s_addr in MCP23017.interrupt_pins):
                MCP23017.interrupt_pins[s_addr] = {}
            if not (s_pin in MCP23017.interrupt_pins[s_addr]):
                MCP23017.interrupt_pins[s_addr][s_pin] = {}
            MCP23017.interrupt_pins[s_addr][s_pin]['edge'] = edge
            MCP23017.interrupt_pins[s_addr][s_pin]['callback'] = callback
            MCP23017.interrupt_pins[s_addr][s_pin]['last'] = 0

        if not MCP23017.intf_en:
            MCP23017.intf_en = True
            threading.Timer(bouncetime / 1000, MCP23017.__check_interrupt,
                            args=(bouncetime, )).start()

    @staticmethod
    def clean_interrupt(addr=None, pins=None):
        """
        清楚中断检测
        :param addr: mcp芯片地址，如果为None，则会清除全部中断检测
        :param pins: 需要清除的引脚编号，可以是单个引脚，也可以是多个引脚的列表。
                     如果为None，则会清除指定MCP芯片全部引脚的中断检测
        :return: 无
        """
        if addr is None:
            MCP23017.intf_en = False
            MCP23017.interrupt_pins.clear()
        else:
            s_addr = "%02X" % addr

            if pins is None:
                if s_addr in MCP23017.interrupt_pins:
                    MCP23017.interrupt_pins.pop(s_addr)
                return

            if isinstance(pins, int):
                pins = [pins]

            for pin in pins:
                s_pin = '%02d' % pin
                if s_addr in MCP23017.interrupt_pins:
                    if s_pin in MCP23017.interrupt_pins[s_addr]:
                        MCP23017.interrupt_pins[s_addr].pop(s_pin)

            if s_addr in MCP23017.interrupt_pins:
                if len(MCP23017.interrupt_pins[s_addr]) == 0:
                    MCP23017.interrupt_pins.pop(s_addr)

            if len(MCP23017.interrupt_pins) == 0:
                MCP23017.intf_en = False
                MCP23017.interrupt_pins.clear()


# Test
if __name__ == '__main__':
    def __cb_io_change(addr, pin, nval, oval):
        pass

    MCP23017.set_out(0x20, MCP23017.ALL, MCP23017.LOW)
    MCP23017.set_dir(0x20, MCP23017.ALL, MCP23017.OUT)

    for i in range(16):
        MCP23017.set_out(0x20, i, MCP23017.HIGH)
        print('输出【%d】已打开。' % int(i+1))
        time.sleep(2)
        MCP23017.set_out(0x20, i, MCP23017.LOW)
        print('输出【%d】已关闭。' % int(i+1))
        time.sleep(2)

    # MCP23017.set_dir(0x20, [0, 1, 2, 3, 4, 5, 6, 7], MCP23017.IN)
    # MCP23017.set_dir(0x20, [8, 9, 10, 11, 12, 13, 14, 15], MCP23017.OUT)
    # MCP23017.set_ipu(0x20, [0, 1, 2, 3, 4, 5, 6, 7], 1)
    # MCP23017.set_interrupt(0x20, [0, 1, 2, 3, 4, 5, 6, 7], MCP23017.RISING, __cb_io_change, 100)

    # for i in range(3):
    #     MCP23017.set_out(0x20, MCP23017.PB0, MCP23017.HIGH)
    #     time.sleep(0.5)
    #     MCP23017.set_out(0x20, MCP23017.PB0, MCP23017.LOW)
    #     time.sleep(0.5)


